Skip to content

SubAgent Events

Filtering and tracking events from nested agents

For Beginners: If you're building a single-agent app (most common), you can skip this entirely. This guide is only needed when using SubAgents (nested agents).

When Do I Need This?

You only need to understand ExecutionContext if you're building applications with nested agents (SubAgents). This includes scenarios like:

  • Agent orchestrator that spawns specialized sub-agents
  • Multi-agent systems with hierarchical delegation
  • Toolkit systems where Toolkits spawn their own agents

For single-agent applications: Ignore ExecutionContext - it will be null or Depth 0, and you don't need to filter anything.

Understanding ExecutionContext

Every event includes an ExecutionContext property that identifies which agent emitted it:

csharp
public record AgentExecutionContext
{
    public required string AgentName { get; init; }      // Name of the agent
    public required string AgentId { get; init; }        // Unique instance ID
    public string? ParentAgentId { get; init; }          // Parent agent (if subagent)
    public IReadOnlyList<string> AgentChain { get; init; } // Full chain from root
    public int Depth { get; init; }                      // Nesting level (0 = root)
    public bool IsSubAgent => Depth > 0;                 // True if not root
}

Depth Levels

Root Agent (Depth = 0)
└── SubAgent 1 (Depth = 1)
    └── SubAgent 2 (Depth = 2)
        └── SubAgent 3 (Depth = 3)

Common Filtering Patterns

Pattern 1: Show Only Root Agent Events

Most UIs only want to show the root agent's output, hiding internal SubAgent chatter:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    // Filter out SubAgent events
    if (evt.ExecutionContext?.IsSubAgent == true) continue;

    // Only root agent events reach here
    HandleEvent(evt);
}

Pattern 2: Indent by Depth

Show all events with indentation based on nesting level:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    var depth = evt.ExecutionContext?.Depth ?? 0;
    var indent = new string(' ', depth * 2);

    switch (evt)
    {
        case TextDeltaEvent delta:
            Console.Write($"{indent}{delta.Text}");
            break;

        case ToolCallStartEvent toolStart:
            var agentName = evt.ExecutionContext?.AgentName ?? "Agent";
            Console.WriteLine($"{indent}[{agentName}] Calling: {toolStart.Name}");
            break;
    }
}

Pattern 3: Filter by Specific Agent

Show events only from a specific agent in the hierarchy:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    // Only show events from "WeatherExpert" agent
    if (evt.ExecutionContext?.AgentName != "WeatherExpert") continue;

    HandleEvent(evt);
}

Pattern 4: Track Events by Agent

Group events by which agent emitted them:

csharp
var eventsByAgent = new Dictionary<string, List<AgentEvent>>();

await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    var agentId = evt.ExecutionContext?.AgentId ?? "root";

    if (!eventsByAgent.ContainsKey(agentId))
        eventsByAgent[agentId] = new List<AgentEvent>();

    eventsByAgent[agentId].Add(evt);
}

// Analyze events per agent
foreach (var (agentId, events) in eventsByAgent)
{
    Console.WriteLine($"Agent {agentId}: {events.Count} events");
}

Hierarchical UI Display

Show the agent hierarchy in your UI:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    var ctx = evt.ExecutionContext;
    if (ctx == null) continue;

    switch (evt)
    {
        case MessageTurnStartedEvent:
            // Show agent hierarchy
            var chain = string.Join(" → ", ctx.AgentChain);
            Console.WriteLine($"\n[Agent Chain: {chain}]");
            break;

        case ToolCallStartEvent toolStart:
            Console.WriteLine($"[{ctx.AgentName} @ Depth {ctx.Depth}] Calling: {toolStart.Name}");
            break;

        case TextDeltaEvent delta:
            // Prefix with agent name if subagent
            if (ctx.IsSubAgent)
                Console.Write($"[{ctx.AgentName}] {delta.Text}");
            else
                Console.Write(delta.Text);
            break;
    }
}

Performance Considerations

Don't Check ExecutionContext Every Time

csharp
// INEFFICIENT: Checks context for every event
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt.ExecutionContext?.Depth > 2) continue;
    if (evt.ExecutionContext?.IsSubAgent == false) continue;
    // ...
}

Filter Once at the Top

csharp
// EFFICIENT: Filter early
await foreach (var evt in agent.RunAsync(messages))
{
    // Fast checks first
    if (evt is IObservabilityEvent) continue;
    if (evt.ExecutionContext?.IsSubAgent == true) continue;

    // Only root agent events reach here
    HandleEvent(evt);
}

Debugging Multi-Agent Systems

Log Agent Activity

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    var ctx = evt.ExecutionContext;
    var prefix = ctx != null
        ? $"[{ctx.AgentName} D{ctx.Depth}]"
        : "[Root]";

    _logger.LogDebug("{Prefix} {EventType}", prefix, evt.GetType().Name);
}

Track SubAgent Spawning

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is NestedAgentInvokedEvent nested)
    {
        Console.WriteLine($"SubAgent spawned: {nested.AgentName}");
        Console.WriteLine($"  Parent: {evt.ExecutionContext?.ParentAgentId}");
        Console.WriteLine($"  Depth: {evt.ExecutionContext?.Depth}");
    }
}

When ExecutionContext is Null

ExecutionContext may be null in these scenarios:

  1. Single-agent applications - No nesting, so context is optional
  2. Legacy events - Events emitted before context was added
  3. Custom events - If you don't set ExecutionContext manually

Safe access pattern:

csharp
var isSubAgent = evt.ExecutionContext?.IsSubAgent ?? false;
var agentName = evt.ExecutionContext?.AgentName ?? "Unknown";
var depth = evt.ExecutionContext?.Depth ?? 0;

Example: Multi-Agent Orchestrator

Complete example showing orchestrator + subagents:

csharp
// Orchestrator spawns specialized agents
var orchestrator = new AgentBuilder()
    .WithName("Orchestrator")
    .WithSubAgent("WeatherExpert")
    .WithSubAgent("NewsExpert")
    .Build();

await foreach (var evt in orchestrator.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    var ctx = evt.ExecutionContext;
    var agentName = ctx?.AgentName ?? "Orchestrator";
    var indent = new string(' ', (ctx?.Depth ?? 0) * 2);

    switch (evt)
    {
        case TextDeltaEvent delta:
            // Show which agent is speaking
            if (ctx?.IsSubAgent == true)
                Console.Write($"{indent}[{agentName}] {delta.Text}");
            else
                Console.Write(delta.Text);
            break;

        case ToolCallStartEvent toolStart:
            Console.WriteLine($"{indent}[{agentName}] Calling: {toolStart.Name}");
            break;

        case MessageTurnFinishedEvent when ctx?.IsSubAgent == true:
            Console.WriteLine($"{indent}[{agentName}] Done");
            break;

        case MessageTurnFinishedEvent:
            Console.WriteLine("\n✓ All agents finished");
            break;
    }
}

Common Patterns

Pattern: Collapse SubAgent Details

Only show SubAgent results, hide intermediate steps:

csharp
var subAgentResults = new Dictionary<string, string>();

await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    var ctx = evt.ExecutionContext;
    if (ctx?.IsSubAgent != true) continue;

    switch (evt)
    {
        case MessageTurnFinishedEvent:
            // SubAgent finished - show accumulated result
            if (subAgentResults.TryGetValue(ctx.AgentId, out var result))
            {
                Console.WriteLine($"\n[{ctx.AgentName} Result]");
                Console.WriteLine(result);
                subAgentResults.Remove(ctx.AgentId);
            }
            break;

        case TextDeltaEvent delta:
            // Accumulate subagent output silently
            var key = ctx.AgentId;
            subAgentResults[key] = (subAgentResults.GetValueOrDefault(key) ?? "") + delta.Text;
            break;
    }
}

See Also

Released under the MIT License.